Service Accounts#3058
Open
GregorShear wants to merge 9 commits into
Open
Conversation
faf9471 to
92dd836
Compare
92dd836 to
35ff08b
Compare
GregorShear
commented
Jun 19, 2026
| .map_err(duplicate_err)?; | ||
|
|
||
| for grant in &grants { | ||
| crate::grants::overwrite_user_grant( |
Contributor
Author
There was a problem hiding this comment.
I have mixed feelings about doing this in the create mutation - maybe we need an "update grant" mutation that lets us downgrade capabilities, and the create mutation just upserts (upgrade capabilities only)?
GregorShear
commented
Jun 19, 2026
GregorShear
commented
Jun 19, 2026
|
|
||
| # Run last so its flakiness can't skip the test steps above. It still gates | ||
| # the job on failure, surfacing genuine Stripe regressions. | ||
| - name: Stripe integration test |
Contributor
Author
There was a problem hiding this comment.
moving this flaky test to the end of the line, at least for now
2052d3d to
03fa4db
Compare
03fa4db to
be5fca4
Compare
GregorShear
commented
Jul 7, 2026
Service accounts and users share the same user_grant authz mechanism, so the grant type isn't service-account-specific. Rename the GraphQL output type ServiceAccountGrant -> UserGrant and its input companion ServiceAccountGrantInput -> UserGrantInput. The service-account mutations (addServiceAccountGrant, etc.) keep their names since they describe operations scoped to a service account.
…te/admin Service-account user_grants now carry capability bundles (the orthogonal authz model) rather than the legacy read/write/admin capability. createServiceAccount and addServiceAccountGrant accept a list of CapabilityBundle per prefix, and UserGrant/UserGrantInput expose bundles in place of capability. overwrite_user_grant writes the bundles array with the legacy capability column set to 'none', so a grant's authority comes entirely from its bundles (per the orthogonal_capabilities migration). upsert_user_grant, used by the human-user invite flow, is left on the legacy capability path. A grant must specify at least one bundle; an empty set is rejected rather than minting a no-op grant. CapabilityBundle gains an async_graphql::Enum derive so it can appear in the schema.
Add a capabilityBundles query returning each CapabilityBundle with a display name, description, and the fine-grained capability bits it expands to. Clients (grant pickers) can enumerate the bundles now grantable to service-account user_grants and explain what each confers, mirroring the existing alertTypes query. Adds all()/display_name()/description() to CapabilityBundle.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Service accounts
Non-login identities for CI/CD and automation. A service account is a real
auth.usersrow authorized through ordinaryuser_grants, so RLS,user_roles()resolution, and role traversal work unchanged. It's addressed by a unique catalog name (e.g.acmeCo/ci-bot) that acts as a management anchor: holders of the new service-account capabilities on a covering prefix manage the account. The account's own access comes solely from its grants, which may span prefixes.Schema: new
internal.service_accountstable (uniquecatalog_name, SP-GiST-indexed for prefix scoping) and arefresh_tokens.created_bycolumn recording the minting admin.API keys are refresh tokens
There is no bespoke credential type.
createApiKeymints a multi-use refresh token owned by the account and returns the bearer secret exactly once, in the base64{id, secret}form thatAuthorization: BearerandPOST /api/v1/auth/tokenalready accept. Revoking zeroesvalid_for: the token becomes inert immediately (every request re-verifies against the database) while the row survives for audit.revokeAllApiKeysis the credential kill switch.Service accounts cannot self-manage credentials:
createRefreshToken/revokeRefreshTokenreject service-account callers, so a leaked key can't mint its own replacements or revoke outside the admin flow. (A residual self-service surface exists via PostgREST'srefresh_tokensgrants; it disappears with PostgREST.)Authorization
Four new fine-grained capabilities (
QueryServiceAccounts,CreateServiceAccount,CreateApiKey,RevokeApiKey), bundled asmanage_service_accountand included inteam_admin:serviceAccountsqueryQueryServiceAccountson a covering prefixcreateServiceAccountCreateServiceAccounton the catalog name, plusCreateGranton each seeded grant prefixaddServiceAccountGrantCreateGranton the granted prefixremoveServiceAccountGrant/removeAllServiceAccountGrantsCreateServiceAccounton the catalog namecreateApiKeyCreateApiKeyon the catalog namerevokeApiKey/revokeAllApiKeysRevokeApiKeyon the catalog nameThe per-prefix
CreateGrantcheck on add is the anti-escalation guard: a manager can't hand an account reach they couldn't grant anyone. Removal has no per-prefix requirement since it only narrows access. Re-adding an existing grant overwrites its capability, so downgrades work in one call.Also in this PR
SensitiveGraphQL scalar for one-time secrets: serializes as a string, redactingDebug, and distinct in the schema so client tooling can recognize and redact it. Applied to API-key, refresh-token, and Stripe setup-intent secrets.POST /api/v1/auth/tokennow share onegenerate_access_token, so credential-error sanitization lives in one place.@service_accounts.estuary.devaddresses must never become a tenant's billing contact.Testing
sqlx tests cover the lifecycle end to end: create with multi-grant seeding, mint, list (including derived
lastUsedAt), revoke and kill switches with idempotency, self-management rejection, duplicate catalog names,validForvalidation, and grant add/remove/overwrite semantics. A second test verifies ateam_adminholder without fullAdmincan manage accounts whileCreateGrantstill bounds escalation. The JWT-signing exchange path is covered by existing pgTAP tests.